﻿using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.IO;
using System.Reflection;
using System.Text;

namespace OpenRiaServices.VisualStudio.DomainServices.Tools
{
    public static class CodeGenUtilities
    {
        /// <summary>
        /// Makes an entity name legal for use in a method name
        /// </summary>
        /// <param name="entityName">The name to convert</param>
        /// <returns>The legal entity name</returns>
        public static string MakeLegalEntityName(string entityName)
        {
            // First character is lower-case, so just return the string as-is.
            if (Char.IsLower(entityName[0]))
            {
                entityName = Char.ToUpperInvariant(entityName[0]) + entityName.Substring(1);
            }
            entityName = entityName.Replace('.', '_');

            return entityName;
        }

        /// <summary>
        /// Makes an entity name legal for use in a method paremeter
        /// </summary>
        /// <param name="entityName">The name to convert</param>
        /// <returns>The legal parameter name</returns>
        public static string MakeLegalParameterName(string entityName)
        {
            // First character must be lower case
            if (Char.IsUpper(entityName[0]))
            {
                entityName = Char.ToLowerInvariant(entityName[0]) + entityName.Substring(1);
            }
            entityName = entityName.Replace('.', '_');

            return entityName;
        }

        /// <summary>
        /// Creates an attribute declaration for the named attribute type
        /// </summary>
        /// <param name="attributeTypeName">Name of attribute type</param>
        /// <returns>A new attribute declaration</returns>
        public static CodeAttributeDeclaration CreateAttributeDeclaration(string attributeTypeName)
        {
            CodeTypeReference attribute = new CodeTypeReference(attributeTypeName);
            return new CodeAttributeDeclaration(attribute);
        }

        /// <summary>
        /// Removes the comment at the start of the generated code about being autogenerated code
        /// </summary>
        /// <param name="s">The string to strip.</param>
        /// <param name="isCSharp">Indicates whether the generated code is C#.</param>
        /// <returns>The code remaining after stripping the comment</returns>
        public static string StripAutoGenPrefix(string s, bool isCSharp)
        {
            string lineCommentStart = isCSharp ? "//" : "'";
            while (s.StartsWith(lineCommentStart, StringComparison.Ordinal))
            {
                int crlfPos = s.IndexOf("\r\n", StringComparison.Ordinal);
                s = s.Substring(crlfPos + 2);
            }
            return s;
        }

        /// <summary>
        /// Generates a test to determine if the given expressions are equal.
        /// </summary>
        /// <param name="clrType">The Type of the values being compared.</param>
        /// <param name="left">The left side of the test.</param>
        /// <param name="right">The right side of the test.</param>
        /// <param name="isCSharp">Indicates whether or not the output should be C# specific.</param>
        /// <returns>A new <see cref="CodeExpression"/>.</returns>
        public static CodeExpression MakeEqual(Type clrType, CodeExpression left, CodeExpression right, bool isCSharp)
        {
            if (isCSharp)
            {
                return new CodeBinaryOperatorExpression(left, CodeBinaryOperatorType.IdentityEquality, right);
            }
            else
            {
                CodeExpression eq;
                if (clrType != null && clrType.IsGenericType && clrType.GetGenericTypeDefinition() == typeof(Nullable<>))
                {
                    eq = new CodeMethodInvokeExpression(left, "Equals", right);
                }
                else if (clrType != null && clrType.IsValueType)
                {
                    eq = new CodeBinaryOperatorExpression(
                            left,
                            CodeBinaryOperatorType.ValueEquality,
                            right);
                }
                else if (clrType == typeof(string))
                {
                    eq = new CodeMethodInvokeExpression(null, "String.Equals", left, right);
                }
                else
                {
                    eq = new CodeMethodInvokeExpression(null, typeof(object).Name + ".Equals", left, right);
                }
                return eq;
            }
        }

        /// <summary>
        /// Generates a test to determine if the given expressions are not equal.
        /// </summary>
        /// <param name="clrType">The Type of the values being compared.</param>
        /// <param name="left">The left side of the test.</param>
        /// <param name="right">The right side of the test.</param>
        /// <param name="isCSharp">Indicates whether or not the output should be C# specific.</param>
        /// <returns>A new <see cref="CodeExpression"/>.</returns>
        public static CodeExpression MakeNotEqual(Type clrType, CodeExpression left, CodeExpression right, bool isCSharp)
        {
            if (isCSharp)
            {
                return new CodeBinaryOperatorExpression(left, CodeBinaryOperatorType.IdentityInequality, right);
            }
            else
            {
                CodeExpression eq = MakeEqual(clrType, left, right, isCSharp);
                return new CodeBinaryOperatorExpression(eq, CodeBinaryOperatorType.ValueEquality, new CodePrimitiveExpression(false));
            }
        }

        /// <summary>
        /// Creates a collection of Xml comments surrounded by start end end tags for the named section
        /// </summary>
        /// <param name="section">Section such as "Summary"</param>
        /// <param name="body">The body of the comment</param>
        /// <returns>A collection of Xml comments that can be added to some member.</returns>
        public static CodeCommentStatementCollection GenerateXmlComments(string section, string body)
        {
            CodeCommentStatementCollection result = new CodeCommentStatementCollection();
            result.Add(new CodeCommentStatement(@"<" + section + ">", true));
            result.Add(new CodeCommentStatement(body, true));
            result.Add(new CodeCommentStatement(@"</" + section + ">", true));
            return result;
        }

        /// <summary>
        /// Generates a single Xml comment, generally used for single-line comments
        /// </summary>
        /// <param name="comment">Entire comment</param>
        /// <returns>The new Xml comment</returns>
        public static CodeCommentStatement GenerateXmlComment(string comment)
        {
            return new CodeCommentStatement(comment, true);
        }

        /// <summary>
        /// Gets a <see cref="CodeTypeReference"/> for a CLR type.
        /// </summary>
        /// <param name="type">A CLR type.</param>
        /// <returns>A <see cref="CodeTypeReference"/> for a CLR type.</returns>
        public static CodeTypeReference GetTypeReference(Type type)
        {
            return GetTypeReference(type, null, null);
        }

        /// <summary>
        /// Gets a <see cref="CodeTypeReference"/> for a CLR type.
        /// </summary>
        /// <param name="type">A CLR type.</param>
        /// <param name="codegenContext">A <see cref="ClientProxyGenerator"/>.</param>
        /// <param name="referencingType">The referencing type.</param>
        /// <returns>A <see cref="CodeTypeReference"/> for a CLR type.</returns>
        public static CodeTypeReference GetTypeReference(Type type, ICodeGenContext codegenContext, CodeTypeDeclaration referencingType)
        {
            if (type.IsPrimitive || type == typeof(void) || type == typeof(decimal) || type == typeof(string) || type == typeof(object))
            {
                return new CodeTypeReference(type);
            }

            if (codegenContext != null && referencingType != null)
            {
                CodeNamespace ns = codegenContext.GetNamespace(referencingType);

                if (ns != null && !ns.Name.Equals(type.Namespace))
                {
                    // If the namespace is already imported, the following line will be a no-op.
                    ns.Imports.Add(new CodeNamespaceImport(type.Namespace));
                }
            }

            if (type.IsArray)
            {
                return new CodeTypeReference(
                    CodeGenUtilities.GetTypeReference(type.GetElementType(), codegenContext, referencingType),
                    type.GetArrayRank());
            }

            if (type.IsGenericType)
            {
                Type[] genericArguments = type.GetGenericArguments();
                CodeTypeReference[] typeArguments = new CodeTypeReference[genericArguments.Length];
                for (int i = 0; i < genericArguments.Length; i++)
                {
                    typeArguments[i] = GetTypeReference(genericArguments[i]);
                }
                return new CodeTypeReference(type.Name, typeArguments);
            }

            return new CodeTypeReference(type.Name);
        }

        /// <summary>
        /// Adds an import to the given namespace if it is not already present
        /// </summary>
        /// <param name="codeNamespace">The namespace to which to add the import</param>
        /// <param name="importNamespace">The import name to add</param>
        /// <returns><c>true</c> means one was added</returns>
        public static bool AddImportIfNeeded(CodeNamespace codeNamespace, string importNamespace)
        {
            if (string.IsNullOrEmpty(importNamespace) || string.Equals(codeNamespace.Name, importNamespace, StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }
            foreach (CodeNamespaceImport import in codeNamespace.Imports)
            {
                if (string.Equals(import.Namespace, importNamespace, StringComparison.OrdinalIgnoreCase))
                {
                    return false;
                }
            }
            codeNamespace.Imports.Add(new CodeNamespaceImport(importNamespace));
            return true;
        }

        /// <summary>
        /// Creates a type declaration with the specified name.
        /// </summary>
        /// <param name="typeName">The name of the type.</param>
        /// <param name="ns">The type's namespace.</param>
        /// <returns>A type declaration.</returns>
        public static CodeTypeDeclaration CreateTypeDeclaration(string typeName, string ns)
        {
            CodeTypeDeclaration decl = new CodeTypeDeclaration(typeName);
            decl.UserData["Namespace"] = ns;
            return decl;
        }

        /// <summary>
        /// Creates a property declaration for buddy class.
        /// </summary>
        /// <param name="codeGenContext">Gode generation context.</param>
        /// <param name="buddyClass"><see cref="CodeTypeDeclaration"/> for a buddy class.</param>
        /// <param name="propertyInfo"><see cref="PropertyInfo"/> for the original property.</param>
        /// <param name="insideNamespace">Whether or not our buddy class is inside a namespace.</param>
        /// <returns>A code snippet for a buddy property.</returns>
        public static CodeSnippetTypeMember CreateAutomaticPropertyDeclaration(ICodeGenContext codeGenContext, CodeTypeDeclaration buddyClass, PropertyInfo propertyInfo, bool insideNamespace)
        {
            // Create a field declaration: public static $propertyType$ $propertyName;
            CodeTypeReference propTypeRef = CodeGenUtilities.GetTypeReference(propertyInfo.PropertyType, codeGenContext, buddyClass);
            CodeMemberField field = new CodeMemberField(propTypeRef, propertyInfo.Name);
            field.Attributes = MemberAttributes.Public;

            CodeSnippetTypeMember property = null;
            CodeGeneratorOptions codeGeneratorOptions = codeGenContext.CodeGeneratorOptions;
            using (StringWriter stringWriter = new StringWriter(System.Globalization.CultureInfo.InvariantCulture))
            {
                // create a StringBuilder with correct identation (we expect identation to stay constant)
                string indentString = codeGeneratorOptions.IndentString;
                StringBuilder stringBuilder = new StringBuilder(indentString);
                stringBuilder.Append(indentString);
                
                // If we're inside an namespace increase indent (in VB there's usually no explicit namespace)
                if (insideNamespace)
                {
                    stringBuilder.Append(indentString);
                }

                // generate the code for a field
                codeGenContext.Provider.GenerateCodeFromMember(field, stringWriter, codeGenContext.CodeGeneratorOptions);
                stringBuilder.Append(stringWriter.GetStringBuilder());
                stringBuilder.Replace(Environment.NewLine, String.Empty);

                // do a manual replace to transform a field into a property
                if (codeGenContext.IsCSharp)
                {
                    stringBuilder.Replace(";", " { get; set; }");
                }
                else
                {
                    // typical VB code gen looks like: Public MyName As Integer
                    // insert 'Property' between 'Public' and 'MyName'
                    int propertyTokenPosition = stringBuilder.ToString().IndexOf("Public", StringComparison.Ordinal) + "Public".Length;
                    stringBuilder.Insert(propertyTokenPosition, " Property");
                }
                // create a code snippet out of resulting code
                property = new CodeSnippetTypeMember(stringBuilder.ToString());
            }
            return property;
        }


        /// <summary>
        /// Determines whether the specified type can be used as a generic type
        /// for the RiaServices generated classes.
        /// 
        /// A type to be a valid generic type parameter must comply with the following
        /// conditions:
        /// - The type must be public
        /// - The type must have a parameter-less constructor. 
        /// 
        /// This is to support generating RiaServices generic DomainService classes 
        /// and generic properties; several of them define TParam to have a default ctr 
        /// For an example see LinqToSqlDomainService or LinqToEntitiesDomainService.
        /// </summary>
        /// <param name="type">The type to check.</param>
        /// <returns>Returns true if the type is a valid generic type param.</returns>
        public static bool IsValidGenericTypeParam(Type type)
        {
            if (type == null || (!type.IsNested && !type.IsPublic) || (type.IsNested && !type.IsNestedPublic) || !type.IsClass)
            {
                return false;
            }

            ConstructorInfo typectr = type.GetConstructor(Type.EmptyTypes);

            // We accept a missing default ctor on abstract types
            return typectr != null || type.IsAbstract;
        }
    }
}
